use std::num::NonZero;

use palette::Palette;

use godot::builtin::{Color, Vector3i};

const DEFAULT_SIZE: usize = 16;

#[repr(usize)]
#[derive(Debug)]
pub enum Neighbor {
    Right,
    Up,
    Back,
    Left,
    Down,
    Forward,
}

impl Neighbor {
    #[inline]
    pub(self) fn get_tuple(self) -> (usize, i32) {
        match self {
            Neighbor::Right => (0, 16),
            Neighbor::Up => (1, 16),
            Neighbor::Back => (2, 16),
            Neighbor::Left => (0, -1),
            Neighbor::Down => (1, -1),
            Neighbor::Forward => (2, -1),
        }
    }
}

impl Into<usize> for Neighbor {
    fn into(self) -> usize {
        self as usize
    }
}

impl From<usize> for Neighbor {
    fn from(mut value: usize) -> Self {
        value = value % 6;

        // SAFETY: value was moded by 6, mapped to Neighbor domain.
        unsafe { std::mem::transmute(value) }
    }
}

impl From<i64> for Neighbor {
    fn from(value: i64) -> Self {
        // SAFETY: value was moded by 6, mapped to Neighbor domain.
        unsafe { std::mem::transmute(value.rem_euclid(6) as usize) }
    }
}

pub struct Chunk {
    size: [usize; 3],
    voxels: Palette<Color>,
}

impl Default for Chunk {
    fn default() -> Self {
        Self::new_cube(DEFAULT_SIZE)
    }
}

impl Chunk {
    pub fn with_size(size: [usize; 3]) -> Self {
        Self {
            size,
            voxels: Palette::new(
                size[0] * size[1] * size[2]
                    + 2 * size[1] * size[2]
                    + 2 * size[0] * size[2]
                    + 2 * size[0] * size[1],
            ),
        }
    }

    pub fn new_cube(size: usize) -> Self {
        Self::with_size([size, size, size])
    }

    pub fn get_volume(&self) -> usize {
        self.size[0] * self.size[1] * self.size[2]
    }

    pub fn get_width(&self) -> i32 {
        self.size[0] as _
    }

    pub fn get_height(&self) -> i32 {
        self.size[1] as _
    }

    pub fn get_length(&self) -> i32 {
        self.size[2] as _
    }

    pub fn get_area(&self, mut d: usize) -> usize {
        d %= 3;
        self.size[(d + 1) % 3] * self.size[(d + 2) % 3]
    }

    pub fn get_voxel_xyz(&self, x: i32, y: i32, z: i32) -> Option<&Color> {
        self.to_position_xyz(x, y, z)
            .and_then(|x| self.voxels.get(x))
    }

    pub fn has_voxel_xyz(&self, x: i32, y: i32, z: i32) -> bool {
        self.get_voxel_xyz(x, y, z).is_some()
    }

    pub fn get_voxel(&self, pos: impl AsPos) -> Option<&Color> {
        let Vector3i { x, y, z } = pos.as_pos();
        self.get_voxel_xyz(x, y, z)
    }

    fn get_index_xyz(&self, x: i32, y: i32, z: i32) -> Option<NonZero<usize>> {
        self.to_position_xyz(x, y, z)
            .and_then(|x| self.voxels.indices.get(x))
            .and_then(|x| NonZero::try_from(*x).ok())
    }

    fn get_index(&self, pos: impl AsPos) -> Option<NonZero<usize>> {
        let Vector3i { x, y, z } = pos.as_pos();
        self.get_index_xyz(x, y, z)
    }

    pub fn has_voxel(&self, pos: impl AsPos) -> bool {
        let Vector3i { x, y, z } = pos.as_pos();
        self.has_voxel_xyz(x, y, z)
    }

    pub fn set_voxel(&mut self, pos: impl AsPos, color: Color) {
        if let Some(local_pos) = self.to_local_position(pos) {
            self.voxels.set(local_pos, color);
        }
    }

    fn set_voxel_raw(&mut self, pos: impl AsPos, color: Color) {
        if let Some(pos) = self.to_position(pos) {
            self.voxels.set(pos, color);
        }
    }

    pub fn remove_voxel(&mut self, pos: impl AsPos) {
        if let Some(local_pos) = self.to_local_position(pos) {
            self.voxels.remove(local_pos);
        }
    }

    fn remove_voxel_raw(&mut self, pos: impl AsPos) {
        if let Some(pos) = self.to_position(pos) {
            self.voxels.remove(pos);
        }
    }

    #[inline]
    pub fn in_bound(&self, d: usize, x: i32) -> bool {
        (0..self.size[d]).contains(&(x as _))
    }

    pub fn in_bound_or(&self, d: usize, x: i32) -> Result<i32, i32> {
        if (0..self.size[d]).contains(&(x as _)) {
            Ok(x)
        } else {
            Err(x)
        }
    }

    pub fn contains_xyz(&self, x: i32, y: i32, z: i32) -> bool {
        self.in_bound(0, x) && self.in_bound(1, y) && self.in_bound(2, z)
    }

    pub fn contains(&self, pos: impl AsPos) -> bool {
        let Vector3i { x, y, z } = pos.as_pos();
        self.contains_xyz(x, y, z)
    }

    pub fn to_local_position_xyz(&self, x: i32, y: i32, z: i32) -> Option<usize> {
        if self.contains_xyz(x, y, z) {
            Some((x + self.size[0] as i32 * (y + z * self.size[1] as i32)) as _)
        } else {
            None
        }
    }

    pub fn to_position_xyz(&self, x: i32, y: i32, z: i32) -> Option<usize> {
        match (
            self.in_bound_or(0, x),
            self.in_bound_or(1, y),
            self.in_bound_or(2, z),
        ) {
            (Ok(x), Ok(y), Ok(z)) => {
                Some((x + self.size[0] as i32 * (y + z * self.size[1] as i32)) as _)
            }
            (Err(-1), Ok(y), Ok(z)) => {
                Some(self.get_volume() + (y + z * self.size[1] as i32) as usize)
            }
            (Err(x), Ok(y), Ok(z)) if x as usize == self.size[0] => Some(
                self.get_volume()
                    + self.get_area(0)
                    + self.get_area(1)
                    + self.get_area(2)
                    + (y + z * self.size[1] as i32) as usize,
            ),
            (Ok(x), Err(-1), Ok(z)) => {
                Some(self.get_volume() + self.get_area(0) + (x + z * self.size[0] as i32) as usize)
            }
            (Ok(x), Err(y), Ok(z)) if y as usize == self.size[1] => Some(
                self.get_volume()
                    + 2 * self.get_area(0)
                    + self.get_area(1)
                    + self.get_area(2)
                    + (x + z * self.size[0] as i32) as usize,
            ),
            (Ok(x), Ok(y), Err(-1)) => Some(
                self.get_volume()
                    + self.get_area(0)
                    + self.get_area(1)
                    + (x + y * self.size[0] as i32) as usize,
            ),
            (Ok(x), Ok(y), Err(z)) if z as usize == self.size[2] => Some(
                self.get_volume()
                    + 2 * self.get_area(0)
                    + 2 * self.get_area(1)
                    + self.get_area(2)
                    + (x + y * self.size[0] as i32) as usize,
            ),
            _ => None,
        }
    }

    pub fn clear_neighbor_cache(&mut self, neighbor: impl Into<Neighbor>) {
        let (d, offset) = neighbor.into().get_tuple();

        let u = (d + 1) % 3;
        let v = (d + 2) % 3;
        for i in 0..self.size[u] as i32 {
            for j in 0..self.size[v] as i32 {
                let mut npos = [0; 3];
                npos[d] = offset;
                npos[u] = i;
                npos[v] = j;

                self.remove_voxel_raw(npos);
            }
        }
    }

    pub fn cache_neighbor(&mut self, neighbor: impl Into<Neighbor>, chunk: &Chunk) {
        if chunk.size != self.size {
            return;
        }

        let (d, offset) = neighbor.into().get_tuple();
        let u = (d + 1) % 3;
        let v = (d + 2) % 3;
        for i in 0..self.size[u] as i32 {
            for j in 0..self.size[v] as i32 {
                let mut npos = [0; 3];
                npos[d] = offset.rem_euclid(self.size[d] as i32);
                npos[u] = i;
                npos[v] = j;

                if let Some(color) = chunk.get_voxel(npos) {
                    npos[d] = offset;
                    self.set_voxel_raw(npos, *color);
                } else {
                    npos[d] = offset;
                    self.remove_voxel_raw(npos);
                }
            }
        }
    }

    pub fn to_position(&self, pos: impl AsPos) -> Option<usize> {
        let Vector3i { x, y, z } = pos.as_pos();
        self.to_position_xyz(x, y, z)
    }

    pub fn to_local_position(&self, pos: impl AsPos) -> Option<usize> {
        let Vector3i { x, y, z } = pos.as_pos();
        self.to_local_position_xyz(x, y, z)
    }

    pub fn greedy_meshing(&self) -> (Vec<[Vector3i; 4]>, Vec<Vector3i>, Vec<Color>) {
        let mut vertices = Vec::new();
        let mut normals = Vec::new();
        let mut colors = Vec::new();

        let mut mask = vec![];
        for d in 0..3 {
            let u = (d + 1) % 3;
            let v = (d + 2) % 3;
            let mut x = [0; 3];
            let mut q = [0; 3];
            mask.clear();
            mask.resize(self.size[u] * self.size[v], None);
            q[d] = 1;

            x[d] = -1;
            while x[d] < self.size[d] as i32 {
                let mut n = 0;
                x[v] = 0;
                while x[v] < self.size[v] as i32 {
                    x[u] = 0;
                    while x[u] < self.size[u] as i32 {
                        mask[n] = match (
                            self.get_index(x.as_pos()),
                            self.get_index(x.as_pos() + q.as_pos()),
                        ) {
                            (Some(idx), None) => Some((idx, true)),
                            (None, Some(idx)) => Some((idx, false)),
                            _ => None,
                        };

                        n += 1;
                        x[u] += 1;
                    }
                    x[v] += 1;
                }
                x[d] += 1;
                //Generate mesh for mask using lexicographic ordering
                n = 0;
                for j in 0..self.size[v] {
                    let mut i = 0;
                    while i < self.size[u] {
                        if let Some((idx, dir)) = mask[n] {
                            let mut w = 1;
                            //Compute width
                            while i + w < self.size[u]
                                && mask[n + w].is_some_and(|x| x.0 == idx && x.1 == dir)
                            {
                                w += 1;
                            }

                            //Compute height
                            let mut h = 1;
                            'done: while j + h < self.size[v] {
                                let mut k = 0;
                                while k < w {
                                    if mask[n + k + h * self.size[u]]
                                        .is_none_or(|x| x.0 != idx || x.1 != dir)
                                    {
                                        break 'done;
                                    }
                                    k += 1;
                                }
                                h += 1;
                            }

                            x[u] = i as i32;
                            x[v] = j as i32;
                            let mut du = [0; 3];
                            let mut dv = [0; 3];

                            let normal = if dir {
                                dv[u] = w;
                                du[v] = h;
                                q.as_pos()
                            } else {
                                du[u] = w;
                                dv[v] = h;
                                -q.as_pos()
                            };

                            vertices.push([
                                x.as_pos(),
                                x.as_pos() + du.as_pos(),
                                x.as_pos() + du.as_pos() + dv.as_pos(),
                                x.as_pos() + dv.as_pos(),
                            ]);

                            normals.push(normal);
                            colors.push(self.voxels.get_value(idx).clone());

                            for l in 0..h {
                                for k in 0..w {
                                    mask[n + k + l * self.size[u]].take();
                                }
                            }
                            i += w;
                            n += w;
                        } else {
                            i += 1;
                            n += 1;
                        }
                    }
                }
            }
        }

        (vertices, normals, colors)
    }
}

pub trait AsPos {
    fn as_pos(&self) -> Vector3i;
}

impl AsPos for Vector3i {
    fn as_pos(&self) -> Vector3i {
        *self
    }
}

impl AsPos for [i32; 3] {
    fn as_pos(&self) -> Vector3i {
        Vector3i::new(self[0], self[1], self[2])
    }
}

impl AsPos for [usize; 3] {
    fn as_pos(&self) -> Vector3i {
        Vector3i::new(self[0] as _, self[1] as _, self[2] as _)
    }
}

impl AsPos for (i32, i32, i32) {
    fn as_pos(&self) -> Vector3i {
        Vector3i::new(self.0, self.1, self.2)
    }
}

impl AsPos for (usize, usize, usize) {
    fn as_pos(&self) -> Vector3i {
        Vector3i::new(self.0 as _, self.1 as _, self.2 as _)
    }
}

#[cfg(test)]
mod test {
    use godot::builtin::{Color, Vector3i};

    use crate::chunk::Chunk;

    #[test]
    pub fn test_chunk() {
        let mut chunk = Chunk::default();
        // chunk.set_voxel(Vector3i::ZERO, Color::BLACK);
        // chunk.set_voxel(Vector3i::RIGHT, Color::RED);
        chunk.set_voxel(Vector3i::UP, Color::GREEN);
        // chunk.set_voxel(Vector3i::BACK, Color::BLUE);

        let (quads, normals, _) = chunk.greedy_meshing();
        for (vertices, Vector3i { x, y, z }) in quads.into_iter().zip(normals) {
            print!("[");
            for Vector3i { x, y, z } in vertices {
                print!("({x},{y},{z})");
            }
            print!("|({x},{y},{z})");
            println!("]")
        }
    }
}
